//
//  Codable+Extension.swift
//  MoonKit
//
//  Created by moon on 2019/3/12.
//  Copyright © 2019 moon. All rights reserved.
//

import Foundation

/// 扩展enum的decode方式，除了从rawValue解码外，还可以自定义一个[Key:T]的映射，将指定的key映射到指定的enum值
public struct RawRepresentableCodingConvertor<Key,T>  where T:RawRepresentable,Key:Hashable {
    public var map:[Key:T]
    public init(_ map:[Key:T]) {
        self.map = map
    }
}
public typealias EnumCodingConvertor = RawRepresentableCodingConvertor
extension RawRepresentableCodingConvertor : CodingConvertor where T.RawValue:Codable,Key:Codable{
    public func decode(from coder: Coder) throws -> T {
        if let rawValue:T.RawValue = try? coder.decode() {
            guard let v = T.init(rawValue: rawValue) else {
                throw CodingError.dataCorrupted(
                    CodingError.Context(codingPath: coder.codingPath,debugDescription: "Cannot initialize \(T.self) from invalid \(T.RawValue.self) value \(rawValue)"))
            }
            return v
        } else if let key:Key = try? coder.decode() {
            guard  let v = self.map[key] else {
                throw CodingError.dataCorrupted(
                CodingError.Context(codingPath: coder.codingPath,debugDescription: "Cannot initialize \(T.self) from invalid \(T.RawValue.self) value \(key)"))
            }
            return v
        }else{
            throw CodingError.dataCorrupted(
            CodingError.Context(codingPath: coder.codingPath,debugDescription: "Cannot initialize \(T.self) from invalid \(T.RawValue.self) value"))
        }
    }
    
    public func encode(to coder: Coder, value: T) throws {
        try coder.encode(value.rawValue)
    }
    
    public typealias CodingType = T
}

/// 使用DateCodingConvertor(.secondsSinceReferenceDate)作为编解码器
extension Date : Codable {
    public static func initObject(from coder: Coder) throws -> Date {
        return try coder.decode(convertor: DateCodingConvertor())
    }
	public mutating func code(coder: Coder) throws {
		self .= (coder,DateCodingConvertor())
	}
}

public struct DateCodingConvertor : CodingConvertor {
	public typealias CodingType = Date
	public enum EncodingStrategy {
		/// 使用与2001/01/01 00:00:00 UTC的间隔的秒数来编解码
		case secondsSinceReferenceDate
		
		/// 使用与1970/01/01 00:00:00 UTC的间隔的秒数来编解码
		case secondsSince1970
		
		/// 使用与1970/01/01 00:00:00 UTC的间隔的毫秒数来编解码
		case millisecondsSince1970
		
		/// 使用 ISO-8601格式的字符串来编解码 (in RFC 3339 format).
		@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
		case iso8601
		
		/// 使用指定日期格式来编解码,如日期格式yyyy-MM-dd HH:mm:ss zz
		/// encode时,将会编码为字符串.此时会造成日期精度损失.比如yyyy-MM-dd HH:mm:ss精度为秒,将会丢失毫秒部分
		case formatted(DateFormatter)
		
		/// 自定义编解码闭包
		case custom((CodingType, Coder) throws -> Void)
		
		public static func dateFormatter(_ dateFormat:String) -> EncodingStrategy {
			let f = DateFormatter()
			f.dateFormat = dateFormat
			return .formatted(f)
		}
	}
	public indirect enum DecodingStrategy {
		/// 使用与2001/01/01 00:00:00 UTC的间隔的秒数来编解码
		case secondsSinceReferenceDate
		
		/// 使用与1970/01/01 00:00:00 UTC的间隔的秒数来编解码
		case secondsSince1970
		
		/// 使用与1970/01/01 00:00:00 UTC的间隔的毫秒数来编解码
		case millisecondsSince1970
		
		/// 使用 ISO-8601格式的字符串来编解码 (in RFC 3339 format).
		@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
		case iso8601
		
		/// 使用指定日期格式来编解码,如日期格式yyyy-MM-dd HH:mm:ss zz
		/// encode时,将会编码为字符串.此时会造成日期精度损失.比如yyyy-MM-dd HH:mm:ss精度为秒,将会丢失毫秒部分
		case formatted(DateFormatter)
		
		/// 链式编解码,按嵌套的规则,进行解码
		case chain([DecodingStrategy])
		
		/// 自定义编解码闭包
		case custom((_ decoder: Coder) throws -> CodingType)
		
		public static func dateFormatter(_ dateFormat:String) -> DecodingStrategy {
			let f = DateFormatter()
			f.dateFormat = dateFormat
			return .formatted(f)
		}
	}
	
	public let encodingStrategy:EncodingStrategy
	public let decodingStrategy:DecodingStrategy
	public init(_ encodingStrategy:EncodingStrategy = .secondsSinceReferenceDate,_ decodingStrategy:DecodingStrategy = .secondsSinceReferenceDate) {
		self.encodingStrategy = encodingStrategy
		self.decodingStrategy = decodingStrategy
	}
	fileprivate func decode(from coder: Coder, strategy: DecodingStrategy) throws -> CodingType {
		switch strategy {
		case .secondsSinceReferenceDate:
			let t:TimeInterval = try coder.decode()
			return Date(timeIntervalSinceReferenceDate: t)
		case .secondsSince1970:
			let t:TimeInterval = try coder.decode()
			return Date(timeIntervalSince1970: t)
		case .millisecondsSince1970:
			let t:TimeInterval = try coder.decode()
			return Date(timeIntervalSince1970: t / 1000.0)
		case .iso8601:
			if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
				let string:String = try coder.decode()
				guard let date = _iso8601Formatter.date(from: string) else {
					throw CodingError.dataCorrupted(CodingError.Context(codingPath: coder.codingPath, debugDescription: "Expected date string \(string) to be ISO8601-formatted."))
				}
				return date
			} else {
				throw CodingError.dataCorrupted(CodingError.Context(codingPath: coder.codingPath, debugDescription: "ISO8601DateFormatter is unavailable on this platform."))
			}
		case .formatted(let formatter):
			let string:String = try coder.decode()
			guard let date = formatter.date(from: string) else {
				throw CodingError.dataCorrupted(CodingError.Context(codingPath: coder.codingPath, debugDescription: "Date string \(string) does not match format expected by formatter \(String(describing: formatter.dateFormat))."))
			}
			return date
		case .chain(let chains):
			guard !chains.isEmpty else {
				throw CodingError.convertFail(CodingError.Context(codingPath: coder.codingPath, debugDescription: "Date decode strategy chain is empty"))
			}
			var chainErrors:[Error] = []
			for s in chains {
				do {
					let v = try decode(from: coder, strategy: s)
					return v
				} catch {
					chainErrors.append(error)
				}
			}
			throw CodingError.convertFail(CodingError.Context(codingPath: coder.codingPath, debugDescription: "Date strategy chain \(chains) decode fail:\(chainErrors)"))
		case .custom(let closure):
			return try closure(coder)
		}
	}
	public func decode(from coder: Coder) throws -> CodingType {
		return try decode(from: coder, strategy: decodingStrategy)
	}
	fileprivate func encode(to coder:Coder,value: CodingType,strategy: EncodingStrategy) throws {
		switch strategy {
		case .secondsSinceReferenceDate:
			var v = value.timeIntervalSinceReferenceDate
			v .= coder
		case .secondsSince1970:
			var v = value.timeIntervalSince1970
			v .= coder
		case .millisecondsSince1970:
			var v = value.timeIntervalSince1970 * 1000.0
			v .= coder
		case .iso8601:
			if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
				var v = _iso8601Formatter.string(from: value)
				v .= coder
			} else {
				throw CodingError.dataCorrupted(CodingError.Context(codingPath: coder.codingPath, debugDescription: "ISO8601DateFormatter is unavailable on this platform."))
			}
		case .formatted(let formatter):
			var v = formatter.string(from: value)
			v .= coder
		case .custom(let closure):
			try closure(value, coder)
		}
	}
	public func encode(to coder:Coder,value: CodingType) throws {
		try encode(to: coder, value: value, strategy: encodingStrategy)
	}
}
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
fileprivate var _iso8601Formatter: ISO8601DateFormatter = {
	let formatter = ISO8601DateFormatter()
	formatter.formatOptions = .withInternetDateTime
	return formatter
}()


/// 使用DataCodingConvertor(.base64)作为编解码器
extension Data : Codable {
    public static func initObject(from coder: Coder) throws -> Data {
        return try coder.decode(convertor: DataCodingConvertor())
    }
	public mutating func code(coder: Coder) throws {
		self .= (coder,DataCodingConvertor())
	}
}

public struct DataCodingConvertor : CodingConvertor {
	public typealias CodingType = Data
	
	public enum EncodingStrategy {
		case base64
		case custom((CodingType, Coder) throws -> Void)
	}
	public enum DecodingStrategy {
		case base64
		case custom((_ decoder: Coder) throws -> CodingType)
	}
	public let encodingStrategy:EncodingStrategy
	public let decodingStrategy:DecodingStrategy
	public init(_ encodingStrategy:EncodingStrategy = .base64,_ decodingStrategy:DecodingStrategy = .base64) {
		self.encodingStrategy = encodingStrategy
		self.decodingStrategy = decodingStrategy
	}
	public func decode(from coder: Coder) throws -> CodingType {
		switch decodingStrategy {
		case .base64:
			let string:String = try coder.decode()
			guard let data = Data(base64Encoded: string) else {
				throw CodingError.dataCorrupted(CodingError.Context(codingPath: coder.codingPath, debugDescription: "Encountered Data is not valid Base64."))
			}
			return data
		case .custom(let closure):
			return try closure(coder)
		}
	}
	public func encode(to coder:Coder,value: CodingType) throws {
		switch encodingStrategy {
		case .base64:
			var v = value.base64EncodedString()
			v .= coder
		case .custom(let closure):
			try closure(value, coder)
		}
	}
}

public struct FloatingPointCodingConvertor<FloatType> : CodingConvertor where FloatType:BinaryFloatingPoint{
	public typealias CodingType = FloatType
	
	public enum Strategy {
		case `throw`
		case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String)
	}
	public let encodingStrategy:Strategy
	public let decodingStrategy:Strategy
	public init(_ encodingStrategy:Strategy = .`throw`,_ decodingStrategy:Strategy = .`throw`) {
		self.encodingStrategy = encodingStrategy
		self.decodingStrategy = decodingStrategy
	}
	public init(_ strategy:Strategy = .`throw`) {
		self.encodingStrategy = strategy
		self.decodingStrategy = strategy
	}
	public func decode(from coder: Coder) throws -> CodingType {
		if let value:Double = try coder.decode() {
			return CodingType(value)
		} else if let value:String = try coder.decode() {
			switch decodingStrategy {
			case .throw :
				throw CodingError._typeMismatch(at: coder.codingPath, expectation: CodingType.self, reality: value)
			case .convertFromString(let positiveInfinity,let negativeInfinity,let nan) :
				switch value {
				case positiveInfinity:
					return CodingType.infinity
				case negativeInfinity:
					return -CodingType.infinity
				case nan:
					return CodingType.nan
				default:
					guard let tmp = Double(value) else {
						throw CodingError._typeMismatch(at: coder.codingPath, expectation: CodingType.self, reality: value)
					}
					return CodingType(tmp)
				}
			}
		} else {
			throw CodingError.dataCorrupted(CodingError.Context(codingPath: coder.codingPath, debugDescription: "decode Float value is failed"))
		}
	}
	public func encode(to coder:Coder,value: CodingType) throws {
		if value == CodingType.infinity {
			switch encodingStrategy {
			case .throw:
				throw CodingError._invalidFloatingPointValue(value, at: coder.codingPath)
			case .convertFromString(let positiveInfinity,_,_):
				try coder.encode(positiveInfinity)
			}
		} else if value == -CodingType.infinity {
			switch encodingStrategy {
			case .throw:
				throw CodingError._invalidFloatingPointValue(value, at: coder.codingPath)
			case .convertFromString(_,let negativeInfinity,_):
				try coder.encode(negativeInfinity)
			}
		} else if value.isNaN{
			switch encodingStrategy {
			case .throw:
				throw CodingError._invalidFloatingPointValue(value, at: coder.codingPath)
			case .convertFromString(_,_,let nan):
				try coder.encode(nan)
			}
		} else {
			try coder.encode(Double(value))
		}
	}
}

/// 使用URLCodingConvertor()作为编解码器
extension URL : Codable {
    public static func initObject(from coder: Coder) throws -> URL {
        return try coder.decode(convertor: URLCodingConvertor())
    }
	public mutating func code(coder: Coder) throws {
		self .= (coder,URLCodingConvertor())
	}
}
public struct URLCodingConvertor : CodingConvertor {
	public typealias CodingType = URL
	private enum __CodingKeys : String, CodingKey {
		case base
		case relative
	}
	public func decode(from coder: Coder) throws -> CodingType {
		if let string:String = try? coder.decode() {
			guard let url = URL(string: string) else {
				throw CodingError.dataCorrupted(CodingError.Context(codingPath: coder.codingPath,debugDescription: "Invalid URL string \(string)."))
			}
			return url
		} else {
			var container:KeyedCodingContainer<__CodingKeys> = try coder.decode()
			let relative = try container.decode(String.self, forKey: .relative)
			let base:URL? = container.contains(.base) ? try container.decodeIfPresent(URL.self, forKey: .base) : nil
			guard let url = URL(string: relative, relativeTo: base) else {
				throw CodingError.dataCorrupted(CodingError.Context(codingPath: coder.codingPath,debugDescription: "Invalid URL string \(relative) \(String(describing: base))."))
			}
			return url
		}
	}
	public func encode(to coder:Coder,value: CodingType) throws {
		if let base = value.baseURL {
			var container:KeyedCodingContainer<__CodingKeys> = try coder.decode()
			try container.encode(value.relativeString, forKey: .relative)
			try container.encode(base, forKey: .base)
		} else {
			try coder.encode(value.relativeString)
		}
	}
}

extension Decimal : Codable {
    public static func initObject(from coder: Coder) throws -> Decimal {
        return try coder.decode(convertor: DecimalCodingConvertor())
    }
	public mutating func code(coder: Coder) throws {
		self .= (coder,DecimalCodingConvertor())
	}
}
public struct DecimalCodingConvertor : CodingConvertor {
	public typealias CodingType = Decimal
	private enum __CodingKeys : String, CodingKey {
		case exponent
		case length
		case isNegative
		case isCompact
		case mantissa
	}
	public func decode(from coder: Coder) throws -> CodingType {
		if let string:String = try? coder.decode() {
			guard let value = Decimal(string:string) else {
				throw CodingError.dataCorrupted(CodingError.Context(codingPath: coder.codingPath,debugDescription: "Invalid Decimal string \(string)."))
			}
			return value
		} else if let number:Double = try? coder.decode() {
			return Decimal.init(number)
		} else if let number:Int = try? coder.decode() {
			return Decimal.init(number)
		} else if let number:Int8 = try? coder.decode() {
			return Decimal.init(number)
		} else if let number:Int16 = try? coder.decode() {
			return Decimal.init(number)
		} else if let number:Int32 = try? coder.decode() {
			return Decimal.init(number)
		} else if let number:Int64 = try? coder.decode() {
			return Decimal.init(number)
		} else if let number:UInt = try? coder.decode() {
			return Decimal.init(number)
		} else if let number:UInt8 = try? coder.decode() {
			return Decimal.init(number)
		} else if let number:UInt16 = try? coder.decode() {
			return Decimal.init(number)
		} else if let number:UInt32 = try? coder.decode() {
			return Decimal.init(number)
		} else if let number:UInt64 = try? coder.decode() {
			return Decimal.init(number)
		} else {
			var container:KeyedCodingContainer<__CodingKeys> = try coder.decode()
			let exponent = try container.decode(CInt.self, forKey: .exponent)
			let length = try container.decode(CUnsignedInt.self, forKey: .length)
			let isNegative = try container.decode(Bool.self, forKey: .isNegative)
			let isCompact = try container.decode(Bool.self, forKey: .isCompact)
			
			var mantissaContainer = try container.nestedUnkeyedContainer(forKey: .mantissa)
			var mantissa: (CUnsignedShort, CUnsignedShort, CUnsignedShort, CUnsignedShort,
				CUnsignedShort, CUnsignedShort, CUnsignedShort, CUnsignedShort) = (0,0,0,0,0,0,0,0)
			mantissa.0 = try mantissaContainer.decode(CUnsignedShort.self)
			mantissa.1 = try mantissaContainer.decode(CUnsignedShort.self)
			mantissa.2 = try mantissaContainer.decode(CUnsignedShort.self)
			mantissa.3 = try mantissaContainer.decode(CUnsignedShort.self)
			mantissa.4 = try mantissaContainer.decode(CUnsignedShort.self)
			mantissa.5 = try mantissaContainer.decode(CUnsignedShort.self)
			mantissa.6 = try mantissaContainer.decode(CUnsignedShort.self)
			mantissa.7 = try mantissaContainer.decode(CUnsignedShort.self)
			
			return  Decimal(_exponent: exponent,
						   _length: length,
						   _isNegative: CUnsignedInt(isNegative ? 1 : 0),
						   _isCompact: CUnsignedInt(isCompact ? 1 : 0),
						   _reserved: 0,
						   _mantissa: mantissa)
		}
	}
	public func encode(to coder:Coder,value: CodingType) throws {
		var container = try coder.container(keyedBy: __CodingKeys.self)
		try container.encode(value._exponent, forKey: .exponent)
		try container.encode(value._length, forKey: .length)
		try container.encode(value._isNegative == 0 ? false : true, forKey: .isNegative)
		try container.encode(value._isCompact == 0 ? false : true, forKey: .isCompact)
		
		var mantissaContainer = try container.nestedUnkeyedContainer(forKey: .mantissa)
		try mantissaContainer.encode(value._mantissa.0)
		try mantissaContainer.encode(value._mantissa.1)
		try mantissaContainer.encode(value._mantissa.2)
		try mantissaContainer.encode(value._mantissa.3)
		try mantissaContainer.encode(value._mantissa.4)
		try mantissaContainer.encode(value._mantissa.5)
		try mantissaContainer.encode(value._mantissa.6)
		try mantissaContainer.encode(value._mantissa.7)
	}
}



import UIKit
extension CGFloat : Codable {
    public static func initObject(from coder: Coder) throws -> CGFloat {
        return try coder.decode(convertor: FloatingPointCodingConvertor<CGFloat>.init(.throw))
    }
    public mutating func code(coder: Coder) throws {
        self .= (coder,FloatingPointCodingConvertor<CGFloat>.init(.throw))
    }
}

extension NSNumber : Codable {
    public func code(coder: Coder) throws {
        if coder.isDecoding {
            throw CodingError.canNotDecodeTo(CodingError.Context(codingPath: coder.codingPath, debugDescription: "Can not decode coder value to NSNumber Object"))
        } else {
           try coder.encode(self.stringValue)
        }
    }
    
    public static func initObject(from coder: Coder) throws -> Self {
        var value:Self?
        if let intValue:Int = try? coder.decode() {
            value = Self(value: intValue)
        } else if let doubleValue:Double = try? coder.decode() {
            value = Self(value: doubleValue)
        } else if let boolValue:Bool = try? coder.decode() {
            value = Self(value: boolValue)
        } else {
            throw CodingError.dataCorrupted(CodingError.Context(codingPath: coder.codingPath, debugDescription: "Expected Int/Double/Bool value"))
        }
        return value!
    }

}
